commonlibsse_ng\rel\module/
module_core.rs

1// C++ Original code
2// - https://github.com/SARDONYX-forks/CommonLibVR/blob/ng/include/REL/Module.h
3// - load_segments, clear: https://github.com/SARDONYX-forks/CommonLibVR/blob/ng/src/REL/Module.cpp
4// SPDX-FileCopyrightText: (C) 2018 Ryan-rsm-McKenzie
5// SPDX-License-Identifier: MIT
6//
7// SPDX-FileCopyrightText: (C) 2025 SARDONYX
8// SPDX-License-Identifier: Apache-2.0 OR MIT
9//! Get the memory range of the exe or dll module that the current process is loading and collect the addresses of each segment
10
11use super::module_handle::{ModuleHandle, ModuleHandleError};
12use super::runtime::Runtime;
13use super::segment::{Segment, SegmentName};
14use crate::rel::version::{FileVersionError, Version, get_file_version};
15use snafu::ResultExt as _;
16use windows::Win32::System::Diagnostics::Debug::{
17    IMAGE_SCN_MEM_EXECUTE, IMAGE_SCN_MEM_WRITE, IMAGE_SECTION_CHARACTERISTICS,
18};
19
20/// Represents a loaded module in memory.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct Module {
23    /// Name of the module. (e.g. `"SkyrimSE.exe"`)
24    pub filename: windows::core::HSTRING,
25    /// File path of the module. (e.g. `"SkyrimSE.exe"`)
26    pub file_path: String,
27    /// Memory segments of the module.
28    pub(crate) segments: [Segment; 8],
29    /// Version information of the module.
30    pub version: Version,
31    /// Base module handle if available.
32    pub base: ModuleHandle,
33    /// Runtime type of the module.
34    pub runtime: Runtime,
35}
36
37impl Module {
38    const SEGMENTS: [(&str, IMAGE_SECTION_CHARACTERISTICS); 8] = [
39        (".text", IMAGE_SCN_MEM_EXECUTE),
40        (".idata", IMAGE_SECTION_CHARACTERISTICS(0)),
41        (".rdata", IMAGE_SECTION_CHARACTERISTICS(0)),
42        (".data", IMAGE_SECTION_CHARACTERISTICS(0)),
43        (".pdata", IMAGE_SECTION_CHARACTERISTICS(0)),
44        (".tls", IMAGE_SECTION_CHARACTERISTICS(0)),
45        (".text", IMAGE_SCN_MEM_WRITE),
46        (".gfids", IMAGE_SECTION_CHARACTERISTICS(0)),
47    ];
48
49    /// Method by which a dummy file(`msvcrt.dll`) is loaded for testing.
50    #[cfg(feature = "test_on_ci")]
51    pub(crate) fn new_with_msvcrt() -> Result<Self, ModuleInitError> {
52        let filename = windows::core::h!("msvcrt.dll");
53        let module_handle = unsafe {
54            ModuleHandle::new(filename).map_err(|_| ModuleInitError::ModuleNameAndHandleNotFound)
55        }?;
56
57        Self::init_inner(filename.clone(), module_handle)
58    }
59
60    #[cfg(feature = "test_on_local")]
61    pub(crate) fn new_from_skyrim_exe() -> Result<Self, ModuleInitError> {
62        let path = crate::rel::module::get_skyrim_exe_path(Runtime::Ae)
63            .ok_or(ModuleInitError::ModuleNameAndHandleNotFound)?;
64        let path = windows::core::HSTRING::from(path.as_path());
65
66        let module_handle = unsafe {
67            windows::Win32::System::LibraryLoader::LoadLibraryW(&path)
68                .map_err(|_| ModuleInitError::ModuleNameAndHandleNotFound)
69        }?;
70        let module_handle =
71            ModuleHandle(unsafe { core::ptr::NonNull::new_unchecked(module_handle.0) });
72
73        Self::init_inner(path, module_handle)
74    }
75
76    /// Initializes a new `Module` instance by detecting the currently loaded module.
77    ///
78    /// This method attempts to retrieve the module information from the `SKSE_RUNTIME`
79    /// or fallback to a predefined list of runtime binaries(e.g. `SkyrimSE.exe`).
80    ///
81    /// # Errors
82    /// An error occurs in the following cases
83    /// - If the module handle could not be obtained.
84    /// - Module version could not be obtained.
85    pub fn new() -> Result<Self, ModuleInitError> {
86        use windows::Win32::System::Environment::GetEnvironmentVariableW;
87        use windows::core::{HSTRING, h};
88
89        #[inline]
90        fn get_module_name_from_skse() -> Option<(HSTRING, ModuleHandle)> {
91            let mut filename = vec![0; windows::Win32::Foundation::MAX_PATH as usize];
92            let filename_len =
93                unsafe { GetEnvironmentVariableW(h!("SKSE_RUNTIME"), Some(&mut filename)) }
94                    as usize;
95
96            let is_failed = filename_len != filename.len() - 1 || filename_len == 0;
97            if is_failed {
98                return None;
99            }
100
101            let filename = HSTRING::from_wide(&filename);
102            // Safety: The `SkyrimSE.exe` loaded by the SKSE runtime survives until the end of program execution.
103            let new_handle = unsafe { ModuleHandle::new(&filename).ok() }?;
104            Some((filename, new_handle))
105        }
106
107        #[inline]
108        fn get_module_handle_from_runtime() -> Option<(HSTRING, ModuleHandle)> {
109            #[cfg(feature = "tracing")]
110            tracing::info!(
111                "Failed to read the `SKSE_RUNTIME` environment variable. Trying to get it from Runtime exe (e.g. `SkyrimSE.exe`) instead..."
112            );
113
114            const RUNTIMES: [&windows::core::HSTRING; 2] =
115                [windows::core::h!("SkyrimSE.exe"), windows::core::h!("SkyrimVR.exe")];
116
117            let mut ret = None;
118            for runtime_name in RUNTIMES {
119                // Safety: Loaded `SkyrimSE.exe` will survive until the end of program execution
120                let module_handle = unsafe { ModuleHandle::new(runtime_name) };
121                if let Ok(new_handle) = module_handle {
122                    ret = Some((runtime_name.clone(), new_handle));
123                    break;
124                }
125            }
126
127            ret
128        }
129
130        let (filename, module_handle) = get_module_name_from_skse()
131            .or_else(get_module_handle_from_runtime)
132            .ok_or(ModuleInitError::ModuleNameAndHandleNotFound)?;
133
134        Self::init_inner(filename, module_handle)
135    }
136
137    #[inline]
138    fn init_inner(
139        filename: windows::core::HSTRING,
140        module_handle: ModuleHandle,
141    ) -> Result<Self, ModuleInitError> {
142        let segments = Self::load_segments(&module_handle).context(SegmentLoadFailedSnafu)?;
143        let (version, runtime) = Self::load_version(&filename).context(VersionLoadFailedSnafu)?;
144        let file_path = filename.to_string();
145
146        Ok(Self { filename, file_path, segments, version, base: module_handle, runtime })
147    }
148
149    /// Gets a specific memory segment by [`SegmentName`].
150    ///
151    /// # Example
152    ///
153    /// ```no_run
154    /// use commonlibsse_ng::rel::module::{ModuleState, SegmentName};
155    ///
156    /// ModuleState::map_active(|module| println!("{:?}", module.segment(SegmentName::Textx))).unwrap();
157    /// ```
158    #[inline]
159    pub const fn segment(&self, name: SegmentName) -> Segment {
160        self.segments[name as usize]
161    }
162
163    #[inline]
164    fn load_segments(module_handle: &ModuleHandle) -> Result<[Segment; 8], ModuleHandleError> {
165        use windows::Win32::System::Diagnostics::Debug::{
166            IMAGE_NT_HEADERS64, IMAGE_SECTION_HEADER,
167        };
168
169        let nt_header = module_handle.try_as_nt_header()?;
170        let section_header_offset = {
171            let optional_header_offset = core::mem::offset_of!(IMAGE_NT_HEADERS64, OptionalHeader);
172            optional_header_offset + nt_header.FileHeader.SizeOfOptionalHeader as usize
173        };
174
175        let section =
176            unsafe { (nt_header as *const IMAGE_NT_HEADERS64).byte_add(section_header_offset) }
177                .cast::<IMAGE_SECTION_HEADER>();
178        let section_len =
179            core::cmp::min(nt_header.FileHeader.NumberOfSections as usize, Self::SEGMENTS.len());
180
181        let mut segments = [Segment::const_default(); 8];
182        for i in 0..section_len {
183            let current_section = unsafe { &*section.add(i) };
184
185            let maybe_found = Self::SEGMENTS.iter().enumerate().find(|(_, elem)| {
186                let maybe_ascii = core::str::from_utf8(&current_section.Name);
187                maybe_ascii.is_ok_and(|section_name| {
188                    elem.0 != section_name
189                        && ((current_section.Characteristics & elem.1)
190                            != IMAGE_SECTION_CHARACTERISTICS(0))
191                })
192            });
193
194            if let Some((idx, _)) = maybe_found {
195                segments[idx] = Segment {
196                    proxy_base: *module_handle,
197                    address: current_section.VirtualAddress,
198                    size: current_section.SizeOfRawData,
199                };
200            }
201        }
202        Ok(segments)
203    }
204
205    #[inline]
206    fn load_version(
207        file_path: &windows::core::HSTRING,
208    ) -> Result<(Version, Runtime), FileVersionError> {
209        let version = get_file_version(file_path)?;
210        let runtime = Runtime::from_version(&version);
211        Ok((version, runtime))
212    }
213}
214
215/// Errors that can occur during module initialization.
216#[derive(Debug, Clone, snafu::Snafu, PartialEq, Eq)]
217pub enum ModuleInitError {
218    /// SKSE or Skyrim exe does not exist or is not loaded into the current process.
219    ModuleNameAndHandleNotFound,
220    /// Module handle operation failed during segment search -> {source}
221    SegmentLoadFailed { source: crate::rel::module::ModuleHandleError },
222
223    /// Failed to load version information. -> {source}
224    #[snafu(display("Failed to load module version"))]
225    VersionLoadFailed { source: crate::rel::version::FileVersionError },
226}
227
228#[cfg(test)]
229mod tests {
230    #[cfg(feature = "test_on_ci")]
231    #[cfg_attr(miri, ignore)]
232    #[test]
233    fn test_module_init() {
234        use super::*;
235
236        // Use `msvcrt.dll` for testing since the dll is always US English and
237        // always loaded in the msvc target when the test is run.
238
239        match dbg!(Module::new_with_msvcrt()) {
240            Ok(module) => {
241                assert!(!module.file_path.is_empty());
242                assert!(!module.filename.is_empty());
243                assert_eq!(module.runtime, Runtime::Se);
244            }
245            Err(err) => panic!("Failed to initialize module: {err}"),
246        }
247    }
248}